Skip to content
100 linesCodeBlameRaw
1import { Stack, router, useLocalSearchParams } from "expo-router";
2import { BellOff, Bell, LogOut, MoreHorizontal, Star, StarOff } from "lucide-react-native";
3import { useState } from "react";
4import { Alert, Pressable, View } from "react-native";
5
6import { Conversation } from "@/components/chat/conversation";
7import { Sheet, SheetAction } from "@/components/chat/sheet";
8import { Empty, Loading, Screen, Text } from "@/components/ui";
9import { useChatActions, useChatSidebar } from "@/lib/chat";
10import { space, useColors } from "@/theme";
11
12/**
13 * A conversation. `channel` is a channel's id, or a channel's name when a
14 * link named it (`/acme/-/chat/general`). The ⋯ button stars, mutes or
15 * leaves it.
16 */
17export default function ConversationScreen() {
18 const params = useLocalSearchParams<{ channel: string }>();
19 const sidebar = useChatSidebar();
20 const colors = useColors();
21 const [menu, setMenu] = useState(false);
22 const entry = sidebar.data?.entries.find((one) => one.channel.id === params.channel || one.channel.name === params.channel);
23 // A link by name waits for the sidebar to say which channel it is.
24 const channelId = entry?.channel.id ?? (/^[a-z]+_/.test(params.channel) ? params.channel : "");
25 const actions = useChatActions(channelId);
26 const dm = entry?.channel.kind === "dm";
27 const title = entry ? (dm ? entry.title : `#${entry.channel.name}`) : "";
28
29 if (!channelId) {
30 return (
31 <Screen>
32 <Stack.Screen options={{ title: "" }} />
33 {sidebar.isPending ? <Loading /> : <Empty title="Not in this conversation" body="It may be private, or you may need to join it first." />}
34 </Screen>
35 );
36 }
37
38 return (
39 <Screen>
40 <Stack.Screen
41 options={{
42 headerTitle: () => (
43 <View>
44 <Text style={{ fontWeight: "700" }} numberOfLines={1}>
45 {title}
46 </Text>
47 {entry?.channel.topic ? (
48 <Text size="small" tone="muted" numberOfLines={1}>
49 {entry.channel.topic}
50 </Text>
51 ) : null}
52 </View>
53 ),
54 headerRight: () =>
55 entry ? (
56 <Pressable accessibilityLabel="Conversation options" hitSlop={10} onPress={() => setMenu(true)}>
57 <MoreHorizontal size={22} color={colors.fg} />
58 </Pressable>
59 ) : null,
60 }}
61 />
62 <Conversation channelId={channelId} thread={null} title={title || "this conversation"} />
63 <Sheet visible={menu} onClose={() => setMenu(false)}>
64 <View style={{ paddingHorizontal: space.lg, paddingBottom: space.sm }}>
65 <Text size="heading">{title}</Text>
66 </View>
67 <SheetAction
68 icon={entry?.starred ? StarOff : Star}
69 label={entry?.starred ? "Unstar" : "Star"}
70 onPress={() => {
71 setMenu(false);
72 actions.preferences.mutate({ starred: !entry?.starred });
73 }}
74 />
75 <SheetAction
76 icon={entry?.muted ? Bell : BellOff}
77 label={entry?.muted ? "Unmute" : "Mute"}
78 onPress={() => {
79 setMenu(false);
80 actions.preferences.mutate({ muted: !entry?.muted });
81 }}
82 />
83 {!dm ? (
84 <SheetAction
85 icon={LogOut}
86 label="Leave channel"
87 danger
88 onPress={() => {
89 setMenu(false);
90 Alert.alert(`Leave ${title}?`, "You can join it again from Browse.", [
91 { text: "Cancel", style: "cancel" },
92 { text: "Leave", style: "destructive", onPress: () => actions.leave.mutate(undefined, { onSuccess: () => router.back() }) },
93 ]);
94 }}
95 />
96 ) : null}
97 </Sheet>
98 </Screen>
99 );
100}